<?php
/**
 * Pagination Class
 *
 * This class displays a pagination navigation bar complete with links to first, last,
 * previous, next, and all pages. This class handles cookie setting, page bounds checking/
 * redirection, appropriate error reporting, CSS styling, and POST/GET retrieval all
 * internally.
 *
 * PHP version 5
 *
 * @author        Dane Gardow <dane.g87@gmail.com>
 * @copyright    2013 Dane Gardow
 * @date            01 January 2013
 * @version        1.0
 * @license        Free
 *
 ******************************************************************************************/

if ( ! class_exists( "WP_Statistics_Pagination" ) ):    // Prevent multiple class definitions

	class WP_Statistics_Pagination {
		/*******************************************************
		 * PROPERTIES / DATA MEMBERS
		 *******************************************************/

		// Edit these as you desire
		const DEFAULT_ENTRIES_DISPLAY = 10;                             // Default number of entries to display per page
		const PAGE_GETVAR_NAME = "page";                         // Name of GET variable name for page values (i.e., example.php?page=1)

		private $_paginationID = "pagination";        // ID Name of pagination object			"pagination" is default
		// used also for form name for select options

		// Do not edit these values; they are simply null initializations
		private $_totalEntries = null;        // Total number of entries (usually supplied by MySQL query)
		private $_pagesPerSection = null;        // Total number of pages displayed per section (supplied by admin)
		private $_entriesPerPage = null;        // Total number of entries displayed per page (supplied by user)
		private $_currentPage = null;        // Current page viewed by user
		private $_displayOptions = array();        // Array of options for viewing how many entries per page (supplied by user)
		private $_errors = array();        // Array of encountered error messages
		private $_styles = array();        // Array of CSS styles for pagination navigation display


		/*******************************************************
		 * CONSTRUCTOR
		 *******************************************************/

		function __construct(
			$totalEntries, $pagesPerSection, $options = "", $paginationID = "", $stylePageOff = "",
			$stylePageOn = "", $styleErrors = "", $styleSelect = ""
		) {
			$this->setPaginationID( $paginationID );            // Set ID name of pagination object
			$this->setTotalEntries( $totalEntries );            // Set total entries
			$this->setPagesPerSection( $pagesPerSection );    // Set pages per section
			$this->setDisplayOptions( $options );                // Set viewing display options
			$this->setEntriesPerPage();                        // Set entries per page (input from POST or cookies)
			$this->setCurrentPage();                        // Set current page (input from GET)
			// ! This function must follow after setEntriesPerPage()
			$this->setStyles( $stylePageOff, $stylePageOn,
				$styleSelect, $styleErrors );    // Set CSS styles for pagination navigation display
		}


		/*******************************************************
		 * UTILITY FUNCTIONS
		 *******************************************************/

		public function deleteCookie()            // deletes the cookie if it exists
		{
			$cookieVar = $this->_getPOSTVarName();

			if ( isset( $_COOKIE[ $cookieVar ] ) )                    // If cookie is set
			{
				$_COOKIE[ $cookieVar ] = "";                    // Clear cookie
				setcookie( $cookieVar, "", time() - 3600, "/" );    // Delete cookie
			}
		}

		private function _getURL( $input = 1 )                            // returns appropriate URL with all GET variables intact
		{                                                                // updates only the particular GET variable in question
			$getVars = $_GET;                                            // Get all GET variables

			/* Uncomment this if you need to exclude any GET variables (due to HTACCESS issues, for example) from being
			*  processed in the ensuing URL. Simply enter in the GET variable name in the unset(...) function below.
			unset($getVars["foo"], $getVars["bar"], ... );	// Remove any so they do not appear in URL
			*/

			$output = '?' . http_build_query( array_merge( $getVars, array( $this->_getIDGETVarName() => $input ) ) );
			$output .= '#' . $this->getPaginationID();    // Add #xxx at the end of URL for auto-scrolling

			return $output;
		}

		private function _isError()                // determines if an error exists and registers any errors
		{
			if ( $this->_errors )                // If error already exists, return
			{
				return true;
			}

			if ( ! $this->_totalEntries )        // If total entries not set
			{
				$this->_errors[] = "The value for <strong>total entries</strong> has not been specified.";
			}

			if ( ! $this->_displayOptions )        // If display options not set
			{
				$this->_errors[] = "The values for <strong>display options</strong> have not been specified.";
			}

			if ( ! $this->_entriesPerPage )        // If entries per page not set
			{
				$this->_errors[] = "The value for <strong>entries per page</strong> has not been specified.";
			}

			if ( ! $this->_currentPage )            // If current page not set
			{
				$this->_errors[] = "The value for <strong>current page</strong> has not been specified.";
			}

			return ( $this->_errors ) ? true : false;
		}

		private function _validEntry( $input )                // determines if input is valid
		{
			if ( is_array( $input ) )                // If array
			{
				foreach ( $input as $element ) {                                                        // Recursion: evaluate each array element
					if ( ! $this->_validEntry( $element ) )                    // If invalid
					{
						return false;
					}
				}

				return true;                                            // If function makes it to this point, it is valid
			} else                                // If not array
			{
				if ( ( preg_match( "/^\d+$/", $input ) && $input > 0 ) || strtolower( $input ) == "all" )    // If positive digit or "all"
				{
					return true;
				} else {
					return false;
				}
			}
		}

		private function _navBox( $text, $destinationPage, $end = 0 )        // returns span-encased link for pagination bar
		{
			switch ( $end ) {
				case 1:
					$title = "First page";
					break;
				case 2:
					$title = "Previous page";
					break;
				case 3:
					$title = "Next page";
					break;
				case 4:
					$title = "Last page (" . $this->getTotalPages() . ")";
					break;
				default:
					$title = "";
					break;
			}

			$title = ( $end > 0 && $title != "" ) ? 'title="' . $title . '"' : '';

			$style = $this->_styles["pageOff"];

			// Determine Style
			$style = ( $this->_currentPage == $destinationPage && ! $end ) ? $this->_styles["pageOn"] : $this->_styles["pageOff"];

			// Determine Link URL/Text
			$url = "";

			if ( $this->_currentPage != $destinationPage            // If current page is not same as destination page
			     && $destinationPage <= $this->getTotalPages()    // and destination page does not exceed last page
			     && $destinationPage >= 1
			)                        // and destination page is not less than first page
			{
				$text = '<a href="' . $this->_getURL( $destinationPage ) . '">' . $text . '</a>'; // then make $text a link
			}

			if ( $style ) {
				$style = 'class="' . $style . '"';
			}

			$onClick = ( $url ) ? "onclick=\"location.href='" . $url . "'\"" : "";    // Determine if span element is clickable

			return '<span ' . $style . $title . ' ' . $onClick . '>' . $text . '</span>';
		}


		/*******************************************************
		 * DISPLAY FUNCTIONS
		 *******************************************************/

		public function display()                // displays the pagination bar
		{
			if ( $this->_isError() )                        // If error encountered, do not display, but display errors
			{
				return $this->displayErrors();
			}

			$firstPage    = 1;
			$previousPage = $this->_currentPage - 1;
			$nextPage     = $this->_currentPage + 1;
			$lastPage     = $this->getTotalPages();

			$totalpages = $this->getTotalPages();

			$pagesPerSection = $this->getPagesPerSection();

			$sectionStart = $this->_currentPage - floor( $pagesPerSection / 2 );    // Section start is current page # minus half the # of pages per section

			if ( $sectionStart <= 0 )                                        // Adjust section start to 1 (first page) if # pages between 1st page
			{
				$sectionStart = 1;
			}                                        // and current page is less than half the # of pages per section

			$sectionEnd = $sectionStart + $pagesPerSection - 1;            // Section end is # pages per section after section start,
			// minus 1 (otherwise # of pages per section will exceed given amount by 1)

			if ( $sectionEnd > $lastPage )                                    // Adjust section end to last page if section end exceeds last page
			{
				$sectionEnd = $lastPage;
			}

			$sectionStart = $sectionEnd - $pagesPerSection + 1;            // Adjust section start to # of pages behind section end

			$output = $this->_navBox( "&lt;&lt;", $firstPage, 1 );    // First page
			$output .= $this->_navBox( "&lt;", $previousPage, 2 );        // Previous page

			for ( $i = $sectionStart; $i <= $sectionEnd; ++ $i ) {
				$output .= $this->_navBox( $i, $i );
			}                    // Pagination

			$output .= $this->_navBox( "&gt;", $nextPage, 3 );            // Next Page
			$output .= $this->_navBox( "&gt;&gt;", $lastPage, 4 );        // Last Page

			return $output;
		}

		public function displayErrors()            // displays the errors encountered
		{
			if ( ! $this->_errors ) {
				return "No errors were encountered.";
			}

			$words = ( count( $this->_errors ) > 1 ) ? "errors were" : "error was";

			// Determine CSS styling for error reporting
			if ( $this->_styles["errors"] ) {
				$css = 'class="' . $this->_styles["errors"] . '"';
			} else {
				$css = '';
			}

			$output = '
		<div ' . $css . '>
		The following ' . $words . ' encountered while using the ' . get_class( $this ) . ' class:<br/><br/>
		<ul>';

			foreach ( $this->_errors as $error ) {
				$output .= '<li>' . $error . '</li>';
			}

			$output .= '
		</ul>
		</div>';

			return $output;
		}

		public function displaySelectInterface()        // displays the <select> interface for choosing display amount
		{
			if ( count( $this->_displayOptions ) < 2 )                // If only 1 or fewer options, do not display
			{
				return;
			}

			if ( $this->_isError() )                                // If error encountered, do not display
			{
				return;
			}

			static $count = 0;    // This counts how many times function is run.
			// This variable value is tacked on the end of the form name which
			// will enable multiple instances of the display interface form

			$paginationID = $this->getPaginationID();

			$formName = $paginationID . '_select_form';

			// Determine CSS styling for <select>
			if ( $this->_styles["select"] ) {
				$css = 'class="' . $this->_styles["select"] . '"';
			} else {
				$css = "";
			}

			$formNumber = ( $count ) ? $count : "";

			$output = '
		<form name="' . $formName . $formNumber . '" method="post" style="display:inline-block;" action="' . $this->_getURL( $this->_currentPage ) . '">
		Show: 
		<select ' . $css . ' name="' . $this->_getPOSTVarName() . '" OnChange ="' . $formName . $formNumber . '.submit()">';

			foreach ( $this->_displayOptions as $line ) {
				if ( $this->_entriesPerPage == $line || $this->_entriesPerPage == $this->_totalEntries )    // If option equals entries per page
				{
					$selected = "selected";
				}                                                            // or entries per page equals total entries
				else                                                                                // then select option, otherwise
				{
					$selected = "";
				}                                                                    // leave unselected

				$output .= '<option value="' . $line . '" ' . $selected . '>' . $line . '</option>';
			}

			$output .= '
		</select>
		<noscript><input type="submit" name="paginationDisplaySubmit" value="Display"/></noscript>
		</form>';

			++ $count;    // Increase # of times this function has run

			return $output;
		}


		/*******************************************************
		 * SET FUNCTIONS
		 *******************************************************/

		public function setCurrentPage()                // sets the currently accessed page from GET value
		{
			$idVar = $this->_getIDGETVarName();

			if ( isset( $_GET[ $idVar ] ) )        // If GET set
			{
				$page = $_GET[ $idVar ];
			}        // Retrieve page from GET
			else {
				$page = 1;
			}                                // Otherwise use first page

			if ( $page < 1 || ! preg_match( "/^\d+$/", $page ) )                        // If page is less than 1 or page value not a digit
			{
				header( "Location: " . $this->_getURL() );        // No argument for _getURL() sets it to 1 (first page)
				exit;
			}

			if ( $page > $this->getTotalPages() && $this->getTotalPages() != 0 )    // If page exceeds last page
			{                                                                    // 2nd condition prevents infinite loop should it equal 0
				header( "Location: " . $this->_getURL( $this->getTotalPages() ) );
				exit;
			}

			$this->_currentPage = $page;
		}

		public function setDisplayOptions( $input )        // sets the user-specified display amount
		{
			if ( ! $this->_validEntry( $input ) )                // If invalid entry encountered, register error and exit function
			{
				if ( is_array( $input ) )                    // If array
				{
					$argument = "";

					foreach ( $input as $key => $element ) {
						if ( $key > 0 ) {
							$argument .= ", ";
						}

						$argument .= $element;            // Save all elements in string
					}
				} else                                    // If not array
				{
					$argument = $input;
				}

				$this->_errors[] = "The value(s) set for <strong>display options</strong> is/are invalid: " . $argument;

				return;
			}

			if ( ! is_array( $input ) && strtolower( $input ) == "all" )    // If Not Array and "All" selected
			{
				$input = $this->_totalEntries;
			}                        // Set total entries value

			$this->_displayOptions = $input;
		}

		public function setEntriesPerPage()                // sets entries per page amount from POST or COOKIE values
		{
			if ( $this->_errors )                                        // If an error, quit
			{
				return;
			}

			$varName = $this->_getPOSTVarName();

			if ( count( $this->_displayOptions ) > 1 )                    // If more than 1 display option
			{
				$value = $this->_displayOptions[0];                    // Default is first selection

				if ( isset( $_POST[ $varName ] ) )                // If POST is set
				{
					if ( in_array( $_POST[ $varName ], $this->_displayOptions ) )        // Check for valid post value
					{
						$value = $_POST[ $varName ];
						setcookie( $varName, $value, 604800 + time(), "/" );        // Set cookie
						$_COOKIE[ $varName ] = $value;
					} else                                                                        // If invalid post value
					{
						$value = self::DEFAULT_ENTRIES_DISPLAY;                                    // Set to default if invalid
					}
				} elseif ( isset( $_COOKIE[ $varName ] ) )            // If POST not set, but COOKIE set
				{
					// Check for valid cookie value
					if ( in_array( $_COOKIE[ $varName ], $this->_displayOptions ) )    // Check for valid cookie value
					{
						$value = $_COOKIE[ $varName ];                            // Set to value if valid
					} else {
						$value = self::DEFAULT_ENTRIES_DISPLAY;                                    // Set to default if invalid
						$this->deleteCookie();                                                    // Delete invalid cookie
					}
				}
			} else            // If only one option, set either to default or displayOptions value
			{
				$value = ( $this->_displayOptions ) ? $this->_displayOptions : self::DEFAULT_ENTRIES_DISPLAY;
			}

			if ( strtolower( $value ) == "all" )                // If set to "All", use total entries
			{
				$value = $this->_totalEntries;
			}

			$this->_entriesPerPage = $value;
		}

		public function setPagesPerSection( $input )        // sets # of pages per section
		{
			if ( ! $this->_validEntry( $input ) ) {
				$this->_errors[] = "The value set for <strong>pages per section</strong> is invalid: " . $input;

				return;
			}

			$this->_pagesPerSection = $input;
		}

		public function setPaginationID( $input ) {
			if ( $input ) {
				if ( preg_match( "/^\d+$/", $input[0] ) )        // Check if first character is a digit
				{
					$this->_errors[] = "The first character of the <strong>pagination ID</strong> cannot be a number: " . $input;

					return;                    // cannot be a digit because variable names cannot start with digits,
				}                            // and this value will be used as a variable name

				$this->_paginationID = $input;
			}
		}

		public function setStyles( $pageOff = "", $pageOn = "", $select = "", $errors = "" )    // sets CSS style class names
		{
			$this->_styles = array(
				"pageOff" => $pageOff,
				"pageOn"  => $pageOn,
				"select"  => $select,
				"errors"  => $errors
			);
		}

		public function setTotalEntries( $input )            // sets total number of entries
		{
			if ( ! $this->_validEntry( $input ) ) {
				$this->_errors[] = "The value set for <strong>total entries</strong> is invalid: " . $input;

				return;
			}

			$this->_totalEntries = $input;
		}


		/*******************************************************
		 * GET FUNCTIONS
		 *******************************************************/

		private function _getIDGETVarName()                    // returns GET variable name for pagination pages
		{
			return $this->getPaginationID() . '-' . self::PAGE_GETVAR_NAME;
		}

		private function _getPOSTVarName()                    // returns POST variable name for select/cookie entities
		{
			return $this->getPaginationID() . 'Display';
		}

		public function getCurrentPage()                    // returns the currently accessed page number
		{
			if ( ! $this->_currentPage )        // If not set, return first page
			{
				return 1;
			} else {
				return $this->_currentPage;
			}
		}

		public function getPagesPerSection()                // returns the # of pages per section
		{
			if ( ! $this->_pagesPerSection )                                    // If not set, set error and return 0
			{
				$this->_errors[] = "The value for <strong>pages per section</strong> has not been set.";

				return 0;
			}

			if ( $this->_pagesPerSection > $this->getTotalPages() )            // If per section is greater than total pages
			{
				return $this->getTotalPages();
			}                            // Return total pages
			else {
				return $this->_pagesPerSection;
			}                            // Otherwise return per section
		}

		public function getPaginationID()                    // returns ID name for pagination object
		{
			return $this->_paginationID;
		}

		public function getTotalPages()                        // returns total pages
		{
			if ( $this->_errors )                                            // If there is an error, return 0
			{
				return 0;
			}

			if ( $this->_entriesPerPage == 0 )                                // Prevent division by zero
			{
				return 0;
			}

			return ceil( $this->_totalEntries / $this->_entriesPerPage );    // Total pages: total # of entries divided by total entries per page
		}

		public function getEntryStart()                        // returns the start entry for the page
		{
			if ( $this->_isError() )            // If error encountered
			{
				return 0;
			}

			return ( $this->_currentPage - 1 ) * $this->_entriesPerPage;    // Entry start: 1 less than current page multiplied by total entries per page
		}

		public function getEntryEnd()                        // returns the last entry for the page
		{
			if ( $this->_isError() )            // If error encountered
			{
				return 0;
			}

			if ( $this->_currentPage == $this->getTotalPages() )            // If current page is last page
			{
				return $this->_totalEntries - $this->getEntryStart();
			}    // then entry end is total entries minus start entry
			else {
				return $this->_entriesPerPage;
			}                            // otherwise entry end is # of entries per page
		}

		public function getEntryEndFF()        // Flat-file version of the above getEntryEnd() function
		{
			if ( $this->_isError() )            // If error encountered
			{
				return 0;
			}

			if ( $this->_currentPage == $this->getTotalPages() )            // If current page is last page
			{
				return $this->_totalEntries;
			}                                // then entry end is total entries minus start entry
			else {
				return $this->getEntryStart() + $this->_entriesPerPage;
			}    // otherwise entry end is # of entries per page after start
		}
	}

endif;    // Prevent multiple class definitions
?>